﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Vowei.Core.Models;

namespace Vowei.Core.Resource
{
    public class WorkingHourCalculator
    {
        private ResourceCalendar _calendar;
        private ICalendarException _calendarException;

        public WorkingHourCalculator(ResourceCalendar calendar) : this(calendar, calendar)
        {
        }

        public WorkingHourCalculator(ResourceAssignment assignment) : this(assignment.WorkCalendar, assignment)
        {
        }

        public WorkingHourCalculator(ResourceCalendar calendar, ICalendarException calendarException)
        {
            _calendar = calendar;
            _calendarException = calendarException;
        }

        public static bool IsWorkingTime(DateTime time, ResourceCalendar calendar, ICalendarException calendarException)
        {
            return new WorkingHourCalculator(calendar, calendarException).IsWorkingTime(time);
        }

        public bool IsWorkingTime(DateTime time)
        {
            var type = IsExceptionWorkingTime(time);

            if (double.IsPositiveInfinity(type))
            {
                return true;
            }
            else if (double.IsNegativeInfinity(type))
            {
                return false;
            }
            else
            {
                foreach (var ww in _calendar.WorkWeeks)
                {
                    if (ww.EffectPeriod != null && !ww.EffectPeriod.Contains(time))
                        continue;

                    var dayofweek = _calendar.Culture.Calendar.GetDayOfWeek(time);
                    foreach (var d in ww.Days)
                    {
                        if (d.DayOfWeek == dayofweek && d.Type == WorkingTimeType.Working)
                        {
                            if (time.TimeOfDay == TimeSpan.Zero)
                                return true;

                            foreach (var period in d.Periods)
                            {
                                if (period.TimePortionContains(time))
                                    return true;
                            }
                        }
                    }
                }
            }

            return false;
        }

        private double IsExceptionWorkingTime(DateTime time)
        {
            foreach (var ep in _calendarException.Exceptions)
            {
                if (ep.Contains(time))
                    return ep.Type == WorkingTimeType.Working ? double.PositiveInfinity : double.NegativeInfinity;
            }

            return double.NaN;
        }

        private TimeSpan IncludeException(DateTime start, DateTime finish)
        {
            var peroid = new Period { Start = start, Finish = finish };
            var result = TimeSpan.Zero;
            foreach (var ep in _calendarException.Exceptions)
            {
                var value = ep.Intersect(peroid);
                if (value != null)
                {
                    // 请假
                    if (ep.Type == WorkingTimeType.Nonworking)
                        result -= CalcNormalWorkingHours(value.Start, value.Finish);
                    // 加班
                    else if (ep.Type == WorkingTimeType.Working)
                        result += value.Duration;
                }
            }

            return result;
        }

        public TimeSpan CalcWorkingHours(DateTime value, ResourceCalendar calendar, ICalendarException calendarException)
        {
            return new WorkingHourCalculator(calendar, calendarException).CalcWorkingHours(value);
        }

        public TimeSpan CalcWorkingHours(DateTime start, DateTime finish, ResourceCalendar calendar, ICalendarException calendarException)
        {
            return new WorkingHourCalculator(calendar, calendarException).CalcWorkingHours(start, finish);
        }

        public TimeSpan CalcWorkingHours(DateTime value)
        {
            return CalcWorkingHours(value.Date, value.Date + TimeSpan.FromDays(1));
        }

        public TimeSpan CalcWorkingHours(DateTime start, DateTime finish)
        {
            return CalcNormalWorkingHours(start, finish) + IncludeException(start, finish);
        }

        private TimeSpan CalcNormalWorkingHours(DateTime start, DateTime finish)
        {
            if (start > finish)
                throw new ArgumentException("Start date must less than finish date.");

            var period = new Period { Start = start, Finish = finish };
            var result = TimeSpan.Zero;
            foreach (var week in _calendar.WorkWeeks)
            {
                var isp = period.Intersect(week.EffectPeriod);
                if (isp == null)
                    continue;

                var weeks = (int)(isp.Duration.TotalDays / 7.0);
                if (weeks > 0)
                {
                    result = CalcWorkingDuration(week);
                    result = TimeSpan.FromMilliseconds(result.TotalMilliseconds * weeks);
                }

                var daySpans = isp.Duration.TotalDays % 7.0;
                var dayOfWeek = _calendar.Culture.Calendar.GetDayOfWeek(isp.Start);
                PeriodTimePortion ptp = null;
                for (int i = 0; i < daySpans; ++i)
                {
                    var day = week.Days.Single(d => d.DayOfWeek == dayOfWeek);
                    ptp = new PeriodTimePortion
                    {
                        Start = i == 0 ? isp.Start.TimeOfDay : TimeSpan.MinValue,
                        Finish = i > (daySpans - 1) ? isp.Finish.TimeOfDay : TimeSpan.MaxValue
                    };
                    result = result.Add(CalcWorkingDuration(day, ptp));

                    dayOfWeek = (DayOfWeek)(((int)dayOfWeek + 1) % 7);
                }

                if ((ptp != null)
                    && (period.Finish.TimeOfDay != TimeSpan.Zero)
                    && (ptp.Finish != period.Finish.TimeOfDay))
                {
                    var day = week.Days.Single(d => d.DayOfWeek == dayOfWeek);
                    ptp = new PeriodTimePortion
                    {
                        Start = TimeSpan.MinValue,
                        Finish = period.Finish.TimeOfDay
                    };
                    result = result.Add(CalcWorkingDuration(day, ptp));
                }
            }

            return result;
        }

        private TimeSpan CalcWorkingDuration(WorkWeek week)
        {
            var result = TimeSpan.Zero;
            foreach (var day in week.Days)
            {
                if (day.Type == WorkingTimeType.Working)
                {
                    result += CalcWorkingDuration(day);
                }
            }

            return result;
        }

        private TimeSpan CalcWorkingDuration(WorkDay day)
        {
            var result = TimeSpan.Zero;
            foreach (var period in day.Periods)
                result += period.Duration;

            return result;
        }

        private TimeSpan CalcWorkingDuration(WorkDay day, PeriodTimePortion period)
        {
            var result = TimeSpan.Zero;
            foreach (var p in day.Periods)
            {
                var isp = p.Intersect(period);

                if (isp != null)
                {
                    if (isp.Start == TimeSpan.Zero && isp.Finish == TimeSpan.Zero)
                        result += TimeSpan.FromHours(24);
                    else
                        result += isp.Duration;
                }
            }

            return result;
        }
    }
}
